/*  

Cafechess Library: Cover basic chess programming needs, like GUI, Engine integration, 
FEN and PGN parsing.

                 \\|//
                 (o o)
--------------ooO-(_)-Ooo----------------------------------
Copyright (C) 2009  By Gregory A. Prentice Snoqualmie, WA
                    www.cafechess.org
                    cafechess@comcast.net
                      Oooo
--------------oooO----(  )---------------------------------
              (  )    (_/
               \_)

Microsoft Public License (Ms-PL)
               
Revisions:

Author        Date    Description:

*/
using System;
using System.Collections;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections.Specialized;
using System.Configuration;

using Cafechess.Threading;
using Cafechess.Text.Commands;

namespace Cafechess.Chess.Engines
{
  /// <summary>
  /// Defines our interface to the XBoard chess engine standards.  We can
  /// attach to, send and receive messages with the chess engine.  The main 
  /// command parsing is performed with regular expressions.  By assigning
  /// a regular expression that extracts the specific engine command, we
  /// should be able to create interfaces to multiple xboard engines.
  /// Below is an example of the app.conf file for the Crafty chess engine
  /// see the demo application that should have been distributed with
  /// this library.  The following is an example of how to add Crafty
  /// directly through Xboard.AddCommand.
  /// <code>
  ///  AddCommand("EngineMove","(^move) +(.*)");
  ///  AddCommand("EngineError, "(^Error) +(.*)");
  ///  AddCommand("EngineIllegalMove","(^Illegal move:?) +(.*)");
  ///  AddCommand("EngineResults","(^[01]-[01]) +(.*)");
  ///  AddCommand("EngineResults","(^1/2-1/2) +(.*)");
  ///  AddCommand("EngineResults",^resign) +(.*)");
  ///  AddCommand("EngineOfferDraw","(^offer draw) +(.*)");
  ///  AddCommand("EngineInfo","(^feature) +(.*)");
  ///  AddCommand("EngineTellOpponent""(^tellopponent) +(.*)");
  ///  AddCommand("EngineTellOthers","(^tellothers) +(.*)");
  ///  AddCommand("EngineTellAll","(^tellall) +(.*)");
  ///  AddCommand("EngineTellUser","(^telluser) +(.*)");
  ///  AddCommand("EngineTellUserError","(^tellusererror) +(.*)");
  ///  AddCommand("EngineAskUser","(^askuser) +(.*)");
  ///  AddCommand("EngineTellIcs","(^tellics) +(.*)");
  ///  AddCommand("EngineTellIcsNoAlias","(^tellicsnoalias) +(.*)");
  ///  AddCommand("EngineAnalysis","([0-9]+)[^-\w]+(-?[0-9]+)\W+([0-9]+)\W+([0-9]+)\W+(.*)");
  /// </code>
  /// </summary>
  abstract public class Engine : IThreadedReaderEvents, IChessEngine, IDisposable
  {
    protected string coEngineName;
    protected Process coEngine;
    protected ProcessStartInfo coInfo;
    protected ConsoleReader coReader;
    protected ThreadedReader coThread;
    protected CommandManager coManager;
    protected string coFilename;
    protected string coDirectory;
    protected string coCommandLine;
    protected bool coAnalyzing;
    protected string coSetBoardFormat;

    public string EngineName
    {
      set { coEngineName = value; }
      get { return coEngineName; }
    }
    public string CommandLine
    {
      set { coCommandLine = value; }
      get { return coCommandLine; }
    }
    public string SetBoardFormat
    {
      set { coSetBoardFormat = value; }
      get { return coSetBoardFormat; }
    }
    public string Filename
    {
      set
      {
        coFilename = value;
        coDirectory = System.IO.Path.GetDirectoryName(coFilename);
      }
      get
      {
        return coFilename;
      }
    }

    public delegate void MoveEventHandler(object sender, System.EventArgs e);
    public event MoveEventHandler EngineMove;
    public delegate void AnalysisEventHandler(object sender, System.EventArgs e);
    public event AnalysisEventHandler EngineAnalysis;
    public delegate void ErrorEventHandler(object sender, System.EventArgs e);
    public event ErrorEventHandler EngineError;
    public delegate void InfoEventHandler(object sender, System.EventArgs e);
    public event InfoEventHandler EngineInfo;
    public delegate void ResultsEventHandler(object sender, System.EventArgs e);
    public event ResultsEventHandler EngineResults;
    public delegate void OfferDrawEventHandler(object sender, System.EventArgs e);
    public event OfferDrawEventHandler EngineOfferDraw;
    public delegate void IllegalMoveEventHandler(object sender, System.EventArgs e);
    public event IllegalMoveEventHandler EngineIllegalMove;
    public delegate void TellOpponentEventHandler(object sender, System.EventArgs e);
    public event TellOpponentEventHandler EngineTellOpponent;
    public delegate void TellOthersEventHandler(object sender, System.EventArgs e);
    public event TellOthersEventHandler EngineTellOthers;
    public delegate void TellAllEventHandler(object sender, System.EventArgs e);
    public event TellAllEventHandler EngineTellAll;
    public delegate void TellUserEventHandler(object sender, System.EventArgs e);
    public event TellUserEventHandler EngineTellUser;
    public delegate void TellUserErrorEventHandler(object sender, System.EventArgs e);
    public event TellUserErrorEventHandler EngineTellUserError;
    public delegate void TellIcsEventHandler(object sender, System.EventArgs e);
    public event TellIcsEventHandler EngineTellIcs;
    public delegate void TellIcsNoAliasEventHandler(object sender, System.EventArgs e);
    public event TellIcsNoAliasEventHandler EngineTellIcsNoAlias;
    public delegate void AskUserEventHandler(object sender, System.EventArgs e);
    public event AskUserEventHandler EngineAskUser;

    /// <summary>
    /// Public constructor that builds an object to handle the interface
    /// to a chess engine that follows the Xboard standards.
    /// </summary>
    /// <param name="ievents">
    /// The events that will get called when the engine has something to say.
    /// </param>
    /// <param name="appconfig">If true then we will load the commands from the app config file.</param>
    public Engine(IChessEngineEvents ievents, ChessEngineInterface ci)
    {
      SetBoardFormat = "setboard {0}";
      coCommandLine = "xboard";
      AddEvents(ievents);
      coManager = new CommandManager(this);
      LoadConfig(ci);
    }
    ChessEngineInterface coChessEngineInterface;

    void LoadConfig(ChessEngineInterface ci)
    {
      coChessEngineInterface = ci;
      string str;
      foreach (System.Reflection.PropertyInfo info in ci.GetType().GetProperties())
      {

        object obj = info.GetValue(ci, null);
        if (obj != null)
        {
          string name = info.Name;
          switch (name)
          {
            case "EngineName":
              EngineName = ((EngineName)obj).value;
              break;
            case "Interface":
              str = ((Interface)obj).value;
              break;
            case "Executable":
              Executable ex = ((Executable)obj);
              Filename = ex.value;
              if (ex.add.Length > 0)
              {
                coCommandLine = "";
                foreach (add param in ex.add)
                {
                  if (param.key.Equals("commandline"))
                  {
                    coCommandLine += string.Format("{0} ", param.value);
                  }
                }
              }
              break;
            default:
              add[] madd = obj as add[];
              if (madd != null)
              {
                foreach (add angry in madd)
                {
                  AddCommand(name, angry.value);
                }
              }
              break;
            //for (int ndx = 0; ndx < ceAdd.Length; ndx++)
            //{
            //  string key = ceAdd[ndx].key;
            //  string value = ceAdd[ndx].value;
            //}
          }
        }
      }
    }

    /// <summary>
    /// Allows commands to be added for regular expression parsing
    /// of the engine's output text.  Note that any command in the list
    /// below followed by an asterics simply shows that this is in a non
    /// command format.  All the others commands expect the regular expression
    /// to parse the and create two groupings.  The first being the text that
    /// represents the command and the other match the parameters of the command.
    /// The example shows the commands added for the Crafty chess engine.
    /// </summary>
    /// <param name="commandName">
    /// One of the following xboard command string:
    /// "EngineMove"
    /// "EngineIllegalMove"
    /// "EngineError"
    /// "EngineResults" *
    /// "EngineOfferDraw" *
    /// "EngineInfo"
    /// "EngineTellOthers"
    /// "EngineTellAll"
    /// "EngineTellUser"
    /// "EngineTellUserError"
    /// "EngineAskUser"
    /// "EngineTellIcs"
    /// "EngineAnalysis" *
    /// </param>
    /// <param name="regularEx">
    /// The regular express that associated with the command and
    /// detects the command and extracts it form the engine output text.
    /// </param>
    /// <returns></returns>
    public bool AddCommand(string commandName, string regularEx)
    {
      bool ok = false;

      switch (commandName)
      {
        case "EngineMove":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineMove));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineMove)));
          ok = true;
          break;
        case "EngineIllegalMove":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineIllegalMove));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineIllegalMove)));
          ok = true;
          break;
        case "EngineError":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineError));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineError)));
          ok = true;
          break;
        case "EngineResults":
          //ConfigureCommand(commandName,false,new Command.ConsumerEventHandler(EngineResults));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, false, new Command.ConsumerEventHandler(EngineResults)));
          ok = true;
          break;
        case "EngineOfferDraw":
          //ConfigureCommand(commandName,false,new Command.ConsumerEventHandler(EngineOfferDraw));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, false, new Command.ConsumerEventHandler(EngineOfferDraw)));
          ok = true;
          break;
        case "EngineInfo":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineInfo));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineInfo)));
          ok = true;
          break;
        case "EngineTellOthers":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineTellOthers));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineTellOthers)));
          ok = true;
          break;
        case "EngineTellAll":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineTellAll));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineTellAll)));
          ok = true;
          break;
        case "EngineTellUser":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineTellUser));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineTellUser)));
          ok = true;
          break;
        case "EngineTellUserError":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineTellUserError));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineTellUserError)));
          ok = true;
          break;
        case "EngineAskUser":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineAskUser));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineAskUser)));
          ok = true;
          break;
        case "EngineTellIcs":
          //ConfigureCommand(commandName,true,new Command.ConsumerEventHandler(EngineTellIcs));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, true, new Command.ConsumerEventHandler(EngineTellIcs)));
          ok = true;
          break;
        case "EngineAnalysis":
          //ConfigureCommand(commandName,false,new Command.ConsumerEventHandler(EngineAnalysis));
          coManager.Add(new Command(regularEx, RegexOptions.Compiled, false, new Command.ConsumerEventHandler(EngineAnalysis)));
          ok = true;
          break;
      }
      return ok;
    }

    /// <summary>
    /// Make sure that the engine is killed as to not leave crafty hanging around
    /// This is incase someone does not clean up nicely.
    /// </summary>
    ~Engine()
    {
      Dispose(false);
    }
    #region IThreadedReaderEvents Members
    /// <summary>
    /// Handles the data coming back from the chess engine.  Here is
    /// the heartbeat of the interface.  We parse the engines output
    /// and then call the correct routine as needed.  Defaulting to
    /// the EngineInfo event if we can't match the command.
    /// </summary>
    /// <param name="oColumnNames"></param>
    /// <param name="oValues"></param>
    /// <returns></returns>
    public bool processRow(IList oColumnNames, IList oValues)
    {
      string data = (string)oValues[0];
      if (false == coManager.Process(data))
      {
        if (EngineInfo != null)
        {
          ChessEventArgs args = new ChessEventArgs(data);
          EngineInfo(this, args);
          args = null;
        }
      }
      return true;
    }
    public void beginProducer()
    {
      // Currently we do not need to do anything here.
    }
    public void endProducer(int count)
    {
      // Currently we do not need to do anything here.
    }

    public void finished()
    {
      // Currently we do not need to do anything here.
    }
    public void beginConsumer()
    {
      // Currently we do not need to do anything here.
    }
    public void endConsumer(int count)
    {
      // Currently we do not need to do anything here.
    }
    #endregion

    public void Initialize()
    {
      if (IsRunning())
      {
        coEngine.StandardInput.WriteLine("uci");
        coEngine.StandardInput.Flush();
      }
    }

    #region IChessEngine Members
    /// <summary>
    /// Starts up the chess engine and the reader thread for the
    /// engines output.
    /// </summary>
    public void Start()
    {
      if (!IsRunning())
      {
        coEngine = new Process();
        coInfo = new ProcessStartInfo();
        coReader = new ConsoleReader(coEngine);
        coThread = new ThreadedReader(coReader);
        coThread.addEvents(this);
        coInfo.FileName = coFilename;
        coInfo.WorkingDirectory = coDirectory;
        coInfo.Arguments = coCommandLine; //"xboard";
        coInfo.RedirectStandardInput = true;
        coInfo.RedirectStandardOutput = true;
        coInfo.UseShellExecute = false;
        coInfo.CreateNoWindow = true;
        coEngine.StartInfo = coInfo;
        coEngine.Start();
        coThread.start();
      }
    }
    /// <summary>
    /// Shuts the engine down cleanly.
    /// </summary>
    public void Stop()
    {
      try
      {
        if (IsRunning())
        {

          if (coEngine != null)
          {
            // Make sure if we are analysing a position
            //  we turn it off first.
            Analyze(false);
            // Now send the command to exit.
            coEngine.StandardInput.WriteLine("quit");
            coEngine.StandardInput.Flush();
            // If the thread has not exited yet then force it to close.
            if (coEngine.HasExited == false)
              coEngine.Kill();
          }
        }
      }
      catch (Exception ex)
      {
        ex.ToString();
      }
      finally
      {
        coEngine = null;
        coInfo = null;
        coReader = null;
        coThread = null;
      }
    }

    abstract public void NewGame();
    //{
    //  if (IsRunning())
    //  {
    //    coEngine.StandardInput.WriteLine("new");
    //    coEngine.StandardInput.Flush();
    //  }
    //}
    /// <summary>
    /// Turn on or off the pondering or thinking of the engine.
    /// </summary>
    /// <param name="turnOn"></param>
    abstract public void Ponder(bool turnOn);
    //{
    //  if (IsRunning())
    //  {
    //    coEngine.StandardInput.WriteLine(turnOn ? "post" : "nopost");
    //    coEngine.StandardInput.Flush();
    //  }
    //}

    /// <summary>
    /// Sends a force current move command to the engine.
    /// </summary>
    abstract public void Force();
    //{
    //  if (IsRunning())
    //  {
    //    coEngine.StandardInput.WriteLine("?");
    //    coEngine.StandardInput.Flush();
    //  }
    //}

    /// <summary>
    /// Puts our chess engine into analysis mode.
    /// </summary>
    /// <param name="turnOn"></param>
    abstract public void Analyze(bool turnOn);
    //{
    //  if (IsRunning())
    //  {

    //    if (coAnalyzing && !turnOn)
    //    {
    //      coAnalyzing = false;
    //      coEngine.StandardInput.WriteLine("nopost");
    //      coEngine.StandardInput.WriteLine("exit");
    //      coEngine.StandardInput.Flush();
    //    }
    //    else if (turnOn && !coAnalyzing)
    //    {
    //      coEngine.StandardInput.WriteLine("post");
    //      coEngine.StandardInput.WriteLine("analyze");
    //      coEngine.StandardInput.Flush();
    //      coAnalyzing = true;
    //    }
    //  }
    //}

    /// <summary>
    /// Set up the position to analyze
    /// </summary>
    /// <param name="fen"></param>
    virtual public void SetBoard(string fen)
    {
      if (IsRunning())
      {
        coEngine.StandardInput.WriteLine(string.Format(SetBoardFormat, fen));
        coEngine.StandardInput.Flush();
      }
    }
    /// <summary>
    /// Responsible for hooking up our event listeners to the chess engine.
    /// </summary>
    /// <param name="ievents"></param>
    public void AddEvents(IChessEngineEvents ievents)
    {
      if (ievents != null)
      {
        EngineMove += new MoveEventHandler(ievents.MakeMove);
        EngineAnalysis += new AnalysisEventHandler(ievents.Analysis);
        EngineError += new ErrorEventHandler(ievents.Error);
        EngineInfo += new InfoEventHandler(ievents.Information);
        EngineResults += new ResultsEventHandler(ievents.Results);
        EngineOfferDraw += new OfferDrawEventHandler(ievents.OfferDraw);
        EngineIllegalMove += new IllegalMoveEventHandler(ievents.IllegalMove);
        EngineTellOpponent += new TellOpponentEventHandler(ievents.TellOpponent);
        EngineTellOthers += new TellOthersEventHandler(ievents.TellOthers);
        EngineTellAll += new TellAllEventHandler(ievents.TellAll);
        EngineTellUser += new TellUserEventHandler(ievents.TellUser);
        EngineTellUserError += new TellUserErrorEventHandler(ievents.TellUserError);
        EngineTellIcs += new TellIcsEventHandler(ievents.TellIcs);
        EngineTellIcsNoAlias += new TellIcsNoAliasEventHandler(ievents.TellIcsNoAlias);
        EngineAskUser += new AskUserEventHandler(ievents.AskUser);
      }
    }
    /// <summary>
    /// Responsible for un-hooking our event listeners to the chess engine.
    /// </summary>
    /// <param name="ievents"></param>
    public void RemoveEvents(IChessEngineEvents ievents)
    {
      EngineMove -= new MoveEventHandler(ievents.MakeMove);
      EngineAnalysis -= new AnalysisEventHandler(ievents.Analysis);
      EngineError -= new ErrorEventHandler(ievents.Error);
      EngineInfo -= new InfoEventHandler(ievents.Information);
      EngineResults -= new ResultsEventHandler(ievents.Results);
      EngineOfferDraw -= new OfferDrawEventHandler(ievents.OfferDraw);
      EngineIllegalMove -= new IllegalMoveEventHandler(ievents.IllegalMove);
      EngineTellOpponent -= new TellOpponentEventHandler(ievents.TellOpponent);
      EngineTellOthers -= new TellOthersEventHandler(ievents.TellOthers);
      EngineTellAll -= new TellAllEventHandler(ievents.TellAll);
      EngineTellUser -= new TellUserEventHandler(ievents.TellUser);
      EngineTellUserError -= new TellUserErrorEventHandler(ievents.TellUserError);
      EngineTellIcs -= new TellIcsEventHandler(ievents.TellIcs);
      EngineTellIcsNoAlias -= new TellIcsNoAliasEventHandler(ievents.TellIcsNoAlias);
      EngineAskUser -= new AskUserEventHandler(ievents.AskUser);
    }
    /// <summary>
    /// Sends a chess move to the engine.
    /// </summary>
    /// <param name="move"></param>
    public void Move(string move)
    {
      if (IsRunning())
      {
        coEngine.StandardInput.WriteLine(move);
        coEngine.StandardInput.Flush();
      }
    }
    public bool IsRunning()
    {
      bool running = false;
      if (coEngine != null)
        running = !coEngine.HasExited;
      return running;
    }
    public bool IsAnalyzing()
    {
      return coAnalyzing;
    }
    #endregion


    #region IDisposable Members
    // Track whether Dispose has been called.
    private bool disposed = false;
    public void Dispose()
    {
      Dispose(true);
      // This object will be cleaned up by the Dispose method.
      // Therefore, you should call GC.SupressFinalize to
      // take this object off the finalization queue 
      // and prevent finalization code for this object
      // from executing a second time.
      GC.SuppressFinalize(this);
    }
    private void Dispose(bool disposing)
    {
      // Check to see if Dispose has already been called.
      if (!this.disposed)
      {
        // If disposing equals true, dispose all managed 
        // and unmanaged resources.
        if (disposing)
        {
          coFilename = null;
          coDirectory = null;
        }
        // Call the appropriate methods to clean up 
        // unmanaged resources here.
        // If disposing is false, 
        // only the following code is executed.
        Stop();
        disposed = true;
      }


    }
    #endregion
  }
}
